﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Management;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Threading;

namespace ULSDeobfuscator {

    public delegate void InsertLogEntries(IList<LogEntry> entries, int count);

    public class LogReader : IDisposable {

        private readonly Thread _worker;
        private readonly LogData _logData;

        // with static variables, it is impossible to manage various sets of files
        // in order to have various sets of files, this info should be in LogData
        private static DateTime s_minDate;
        private static DateTime s_maxDate;

        /// <summary>
        /// Map of process Ids to app pool names. If null, it means that we are not allowed to read the command line of process (i.e. we are not admins)
        /// </summary>
        private static Dictionary<int, string> s_processIdMap = new Dictionary<int, string>();

        private class LogData {

            public AutoResetEvent WorkEvent { get; set; }
            public string FileToRead { get; set; }
            public bool IsDone { get; set; }
            public bool ShouldTerminate { get; set; }
            public bool ShouldStopCurrentTask { get; set; }
            public InsertLogEntries InsertLogEntries { get; set; }

            public bool RemoveDuplicates { get; set; }

            public Dispatcher Dispatcher { get; set; }
            // called if an error occurs. 1st string is error message, 2nd string is involved file path
            public Action<string, string> ErrorHandlingDelegate { get; set; }
            // called when a file is done reading
            public Action EndDelegate { get; set; }

            /// <summary>
            /// If non zero, reading will start at this offset
            /// </summary>
            public long OffsetInFile { get; set; }

            // write op
            public string FileToWrite { get; set; }
            public IEnumerable<LogEntry> Entries { get; set; }
        }

        public LogReader() {

            _worker = new Thread(Run) {
                Name = "Log Reader"
            };

            _logData = new LogData {
                    WorkEvent = new AutoResetEvent(false),
                    IsDone = true,
                    Dispatcher = Dispatcher.CurrentDispatcher,
            };
            _worker.Start(_logData);
        }

        static LogReader() {
            ResetDayStatistics();
        }

        public bool IsDone {
            get { return _logData.IsDone; }
        }

        public static DateTime MinDate {
            get {
                return s_minDate;
            }
        }

        public static DateTime MaxDate {
            get {
                return s_maxDate;
            }
        }

        public static void ResetDayStatistics() {
            
            s_minDate = DateTime.MaxValue;
            s_maxDate = DateTime.MinValue;
        }

        public void ReadLogFile(string logFilePath, InsertLogEntries ile, 
                                bool removeDuplicates,
                                Action<string, string> errorHandlingDelegate, Action endDelegate, long readOffset = 0) {

            if (!IsDone) {
                throw new InvalidOperationException("Cannot read or write 2 files at the same time");
            }

            _logData.FileToRead = logFilePath;
            _logData.FileToWrite = null;
            _logData.InsertLogEntries = ile;
            _logData.ErrorHandlingDelegate = errorHandlingDelegate;
            _logData.IsDone = false;
            _logData.EndDelegate = endDelegate;
            _logData.RemoveDuplicates = removeDuplicates;
            _logData.OffsetInFile = readOffset;
            _logData.WorkEvent.Set();
        }

        public void SaveLogFile(string logFilePath, IEnumerable<LogEntry> entries,
                                Action<string, string> errorHandlingDelegate, Action endDelegate) {

            if (!IsDone) {
                throw new InvalidOperationException("Cannot read or write 2 files at the same time");
            }

            _logData.FileToWrite = logFilePath;
            _logData.FileToRead = null;
            _logData.Entries = entries;
            _logData.ErrorHandlingDelegate = errorHandlingDelegate;
            _logData.IsDone = false;
            _logData.EndDelegate = endDelegate;
            _logData.WorkEvent.Set();
        }

        public bool IsBusy {
            get {
                return ! _logData.IsDone;
            }
        }

        public void CancelCurrentTask() {
            
            if (_logData.IsDone) {
                // nothing to do, we were not busy
                return;
            }

            _logData.ShouldStopCurrentTask = true;
        }

        private static void Run(object obj) {

            LogData ld = (LogData) obj;

            while (true) {

                if (ld.ShouldTerminate) {
                    return;
                }

                ld.WorkEvent.WaitOne();

                // if the offset is not null, we wait a bit before reading the file. 
                // As we get the size change notifications as soon as they appear, it happens often that the file is still in use when we try to read it
                // opening with share=read/write helps to read the file, but it seems wiser to keep this small sleep time
                if (ld.OffsetInFile != 0) {
                    Thread.Sleep(100);
                }

                if (ld.ShouldTerminate) {
                    return;
                }

                if (ld.FileToWrite != null) {

                    DoWriteLogFile(ld);
                } else if (ld.FileToRead != null) {
                    ReadAppPoolProcessIds();
                    DoReadLogFile(ld);
                }
                ld.IsDone = true;
                if (ld.EndDelegate != null && ! ld.ShouldStopCurrentTask) {
                    ld.Dispatcher.BeginInvoke(ld.EndDelegate);
                }

                ld.ShouldStopCurrentTask = false;
            }
        }

        private static void ReadAppPoolProcessIds() {
            
            if (s_processIdMap == null) {

                // not admin => give up; this will not change for the lifetime of the application
                return;
            }

            SelectQuery query = new SelectQuery("Win32_Process" , "Name=\"w3wp.exe\"");

            // Initialize an object searcher with this query
            ManagementObjectSearcher searcher =
               new ManagementObjectSearcher(query);

            Regex appSearch = new Regex("-ap \"(.*?)\"");
            foreach (ManagementObject envVar in searcher.Get()) {

                object commandLineAsObject = envVar["CommandLine"];
                if (commandLineAsObject == null) {

                    // we cannot read the command line => give up
                    s_processIdMap = null;
                    return;
                }

                string commandLine = commandLineAsObject.ToString();

                Match m = appSearch.Match(commandLine);
                if (m.Success) {

                    if (m.Groups.Count >= 2) {

                        object processIdAsObject = envVar["ProcessId"];
                        if (processIdAsObject != null) {
                            string processIdAsString = processIdAsObject.ToString();

                            int processId;
                            if (Int32.TryParse(processIdAsString, out processId)) {

                                string appPoolName = m.Groups[1].Value;
                                s_processIdMap[processId] = appPoolName;
                            }
                        }
                    }
                }
            }
        }

        private static void DoReadLogFile(LogData ld) {

            try {
                using (
                        FileStream fs = new FileStream(ld.FileToRead, FileMode.Open, FileAccess.Read,
                                                       FileShare.ReadWrite)) {
                    using (StreamReader sr = new StreamReader(fs)) {

                        string header = sr.ReadLine();
                        if (!AnalyzeHeader(header)) {

                            SignalError(ld,
                                        "The file {0} does not seem to be a SharePoint log file, as it is missing a standard header",
                                        ld.FileToRead);
                            return;
                        }
                        const int BufferSize = 3000;
                        const int OverflowSize = 50; // some slack to have the space to finish some incomplete messages 
                        LogEntry[] buffer = new LogEntry[BufferSize + OverflowSize];
                        int index = 0;
                        int incompleteMessagesCount = 0;
                        const string ContinuationMarker = "...";
                        const string TimestampContinuationMarker = "*";

                        string lastMessage = null;

                        // if asked, start from a given position
                        if (ld.OffsetInFile != 0) {
                            sr.DiscardBufferedData();
                            sr.BaseStream.Seek(ld.OffsetInFile, SeekOrigin.Begin);
                        }

                        while (!sr.EndOfStream) {

                            string line = sr.ReadLine();

                            if (ld.ShouldStopCurrentTask) {
                                break;
                            }

                            if (String.IsNullOrEmpty(line)) {
                                continue; // skip empty line
                            }

                            bool areWeDoneWithThisLine = false;

                            int charsUsedSoFar = 0;
                            string timestamp = GetNextField(line, ref charsUsedSoFar);
                            string process = GetNextField(line, ref charsUsedSoFar);
                            string tid = GetNextField(line, ref charsUsedSoFar);
                            string area = GetNextField(line, ref charsUsedSoFar);
                            string category = GetNextField(line, ref charsUsedSoFar);
                            string eventID = GetNextField(line, ref charsUsedSoFar);
                            string level = GetNextField(line, ref charsUsedSoFar);
                            string message = GetNextField(line, ref charsUsedSoFar);
                            string correlation = GetNextField(line, ref charsUsedSoFar);

                            // is this a new message or a continuation ?
                            bool isContinuation = false;

                            if (message.StartsWith(ContinuationMarker)) {

                                isContinuation = true;
                                message = message.Substring(ContinuationMarker.Length); // remove continuation marker
                            }

                            if (timestamp.EndsWith(TimestampContinuationMarker)) {
                                isContinuation = true;
                                timestamp = timestamp.Substring(0, timestamp.Length - TimestampContinuationMarker.Length);
                                // remove continuation marker
                            }

                            // is it the end of a message ? (1.12: removed)
//                            bool isLineComplete = true;

                            if (message.EndsWith(ContinuationMarker)) {
//                                isLineComplete = false;
                                message = message.Substring(0, message.Length - ContinuationMarker.Length);

                                // incomplete message
                                // if this is the start of a message, we can increment our count of incomplete messages
                                if (!isContinuation) {
                                    incompleteMessagesCount++;
                                }
                            } else {

                                // complete message
                                // if this was a continuation, we can decrement our count of incomplete messages
                                if (isContinuation) {
                                    incompleteMessagesCount--;
                                }
                            }

                            DateTime dt;
                            DateTime.TryParse(timestamp, CultureInfo.InvariantCulture,
                                              DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal, out dt);

                            if (isContinuation) {

                                // find which message we're continuing, from the bottom of the stack
                                // note: variables should be named potentialXXX (potentialContinuedMessageIndex, potentialContinuedEntry)
                                bool foundContinuedMessage = false;
                                LogEntry continuedEntry = null;
                                for (int continuedMessageIndex = index - 1;
                                     continuedMessageIndex >= 0;
                                     continuedMessageIndex--) {

                                    continuedEntry = buffer[continuedMessageIndex];

                                    if (/*!continuedEntry.IsComplete && */ continuedEntry.Day == dt && continuedEntry.Tid == tid) {

                                        foundContinuedMessage = true;
                                        break;
                                    }


                                }

                                if (foundContinuedMessage) {

                                    // just append the message, no need to do anything else
                                    continuedEntry.Message += message;

                                    // no need to test if (isLineComplete)
//                                    continuedEntry.IsComplete = isLineComplete;
                                    // some people like to know about this
                                    continuedEntry.IsMultiLine = true;

                                    areWeDoneWithThisLine = true;
                                } // else not found... not much to do but create a new entry
                            }

                            if (!areWeDoneWithThisLine) {

                                // this is either a new line or a continuation line where the message beginning could not be found

                                bool canAddEntry = true;

                                if (ld.RemoveDuplicates) {

                                    if (message == lastMessage) {

                                        // message is a duplicate => ignore
                                        canAddEntry = false;
                                    }
                                }

                                if (canAddEntry) {

                                    string processInformation = null;
                                    if (s_processIdMap != null) {
                                        ReplaceProcessIdByAppPoolName(ref process, ref processInformation);
                                    }
                                    // do not let empty strings as is
                                    const string EmptyStringReplacement = "(empty)";
                                    LogEntry le = new LogEntry {
                                            Day = dt,
                                            Process = String.IsNullOrEmpty(process) ? EmptyStringReplacement : process,
                                            ProcessInformation = processInformation,
                                            Tid = tid,
                                            Area = String.IsNullOrEmpty(area) ? EmptyStringReplacement : area,
                                            Category = String.IsNullOrEmpty(category) ? EmptyStringReplacement : category,
                                            EventID = String.IsNullOrEmpty(eventID) ? EmptyStringReplacement : eventID,
                                            Level = String.IsNullOrEmpty(level) ? EmptyStringReplacement : level,
                                            Message = message,
//                                            IsComplete = isLineComplete,
                                            Correlation = String.IsNullOrEmpty(correlation)
                                                            ? EmptyStringReplacement
                                                            : correlation,
                                    };

                                    if (dt < MinDate) {
                                        s_minDate = dt;
                                    }
                                    if (dt > MaxDate) {
                                        s_maxDate = dt;
                                    }

                                    buffer[index++] = le;

                                    lastMessage = message;

                                    // if buffer is full, send it to our list
                                    if (index >= BufferSize) {

                                        // either we found all messages till their end or our buffer is really full
                                        if (incompleteMessagesCount == 0 || index == BufferSize + OverflowSize) {
                                            ld.Dispatcher.Invoke(ld.InsertLogEntries, DispatcherPriority.Background,
                                                                 buffer,
                                                                 index);
                                            index = 0;
                                        }
                                    }
                                }
                            }

                        }

                        // flush buffer at end of file
                        if (index > 0 && ! ld.ShouldStopCurrentTask) {

                            ld.Dispatcher.Invoke(ld.InsertLogEntries, DispatcherPriority.Background, buffer, index);
                        }
                    }
                }
            } catch (UnauthorizedAccessException) {

                if (IsUserAdministrator()) {

                    SignalError(ld, "Security permissions prevent the reading of file {0}", ld.FileToRead);
                } else {
                    
                    SignalError(ld,
                                "Security permissions prevent the reading of file {0}." + Environment.NewLine + Environment.NewLine +
                                "Try running the deobfuscator in elevated mode.", ld.FileToRead);
                }
            } catch (Exception ex) {
                
                SignalError(ld, "Error while reading file {0}:" + Environment.NewLine + "{1}", ld.FileToRead, ex.ToString());
            }
        }

        private static void ReplaceProcessIdByAppPoolName(ref string process, ref string processInformation) {
            
            // format seems to be consistent
            // process is w3wp.exe (0xABCD)

            const int StartOfProcessId = 10;
            const int HexadecimalSpecifierLength = 2;
            const int ProcessIdLength = 4;

            if (process.StartsWith("w3wp.exe (")) {

                int processId;
                string processIdAsString = process.Substring(StartOfProcessId + HexadecimalSpecifierLength, ProcessIdLength);
                if (Int32.TryParse(processIdAsString, NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out processId)) {

                    string appPoolName;
                    if (s_processIdMap.TryGetValue(processId, out appPoolName)) {

                        process = "App Pool: " + appPoolName;
                        processInformation = "Original process id: 0x" + processIdAsString;
                    }
                }
            }
        }

        private static void SignalError(LogData ld, string errorMessage, params object[] parameters) {

            string message = String.Format(errorMessage, parameters);
            ld.Dispatcher.BeginInvoke(ld.ErrorHandlingDelegate, message, ld.FileToRead ?? ld.FileToWrite);
        }

        private static bool AnalyzeHeader(string header) {

            if (String.IsNullOrEmpty(header)) {
                return false;
            }

            if (!header.StartsWith("Timestamp")) {
                return false;
            }

            if (!header.Contains("Process")) {
                return false;
            }

            // is it useful to do more ?
            return true;
        }

        private static string GetNextField(string text, ref int startIndex) {

            try {
                if (text.Length < startIndex) {
                    return String.Empty;
                }
                int nextTab = text.IndexOf('\t', startIndex);

                string result;

                if (nextTab == -1) {
                    result = text.Substring(startIndex);
                    startIndex = text.Length - 1;
                } else {

                    // if startIndex = 0, nextTab = 4 => length = 4
                    // 01234 567
                    // abcd\tefg
                    result = text.Substring(startIndex, nextTab - startIndex);
                    startIndex = nextTab + 1;
                }

                return result.Trim();
            } catch (ArgumentOutOfRangeException) {
                return String.Empty;
            }
        }

        private static void DoWriteLogFile(LogData ld) {

            try {
                using (FileStream fs = new FileStream(ld.FileToWrite, FileMode.Create, FileAccess.Write, FileShare.None)) {
                    using (StreamWriter sw = new StreamWriter(fs, Encoding.Unicode)) {

                        // write header
                        sw.WriteLine("Timestamp              	Process                                 	TID   	Area                          	Category                      	EventID	Level     	Message 	Correlation");

                        // write lines
                        foreach (LogEntry le in ld.Entries) {
                            sw.Write(le.Day.Month.ToString("00"));
                            sw.Write('/');
                            sw.Write(le.Day.Day.ToString("00"));
                            sw.Write('/');
                            sw.Write(le.Day.Year);
                            sw.Write(' ');
                            sw.Write(le.Day.Hour.ToString("00"));
                            sw.Write(':');
                            sw.Write(le.Day.Minute.ToString("00"));
                            sw.Write(':');
                            sw.Write(le.Day.Second.ToString("00"));
                            sw.Write('.');
                            sw.Write((le.Day.Millisecond / 10).ToString("00"));
                            sw.Write(" \t");

                            sw.Write("{0,-40}\t", le.Process);
                            sw.Write("{0,-6}\t", le.Tid);
                            sw.Write("{0,-30}\t", le.Area);
                            sw.Write("{0,-30}\t", le.Category);
                            sw.Write("{0,-8}\t", le.EventID);
                            sw.Write("{0,-10}\t", le.Level);
                            sw.Write("{0} \t", le.Message);
                            sw.WriteLine(le.Correlation);
                        }
                    }
                }
            } catch (IOException io) {

                SignalError(ld, "Error while writing file {0}:" + Environment.NewLine + "{1}", ld.FileToWrite, io.ToString());
            }
        }

        // taken from http://stackoverflow.com/questions/1089046/in-net-c-test-if-user-is-an-administrative-user
        // elevated administrator -> true, non-elevated administrator -> false, standard user -> false
        private static bool IsUserAdministrator()
        {
            //bool value to hold our return value
            bool isAdmin;
            try
            {
                //get the currently logged in user
                WindowsIdentity user = WindowsIdentity.GetCurrent();
                if (user == null) {
                    return false;
                }
                WindowsPrincipal principal = new WindowsPrincipal(user);
                isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
            }
            catch (Exception)
            {
                isAdmin = false;
            }
            return isAdmin;
        }

        #region Implementation of IDisposable

        public void Dispose() {

            _logData.ShouldTerminate = true;
            _logData.WorkEvent.Set();

            // it occurred that we got stuck here. the log reader thread was blocked in ld.Dispatcher.Invoke
            // no idea why
            _worker.Join(1000);
            _logData.WorkEvent.Close();
        }

        #endregion
    }


}
